﻿using System;
using System.Collections.Generic;
using System.Drawing;
using NGridDrawer.Partitions;
using NGridDrawer.Report.Layout;

namespace NGridDrawer.Report
{
    /// <LastUpdate author="Blaise Braye" Date="2/07/2008 21:15"/>
    /// <summary>
    /// <see cref="IPartitionDrawer"/> decorator.
    /// associate <see cref="IBlock"/> instances to rows in order to they appear on the same page 
    /// (interest with tooltips or comments for instance)
    /// </summary>
    public class GridAnnotatedDrawer : IPartitionDrawer
    {
        /// <summary>
        /// 	<para>Initializes an instance of the <see cref="GridAnnotatedDrawer"/> class.</para>
        /// </summary>
        /// <param name="drawer">drawer to encapsulate (see global remark about decorator pattern)
        /// </param>
        public GridAnnotatedDrawer(IPartitionDrawer drawer)
        {
            DecoratedDrawer = drawer;
            _attachedToRows = new Dictionary<BlockIndexedByPartitionColumn, IBlock>();
        }

        private IPartitionDrawer decoratedDrawer { get; set; }

        private readonly Dictionary<BlockIndexedByPartitionColumn, IBlock> _attachedToRows;

        /// <summary>
        /// Contains a list of attached <see cref="IBlock"/> instance indexed by the associated row key
        /// </summary>
        protected Dictionary<BlockIndexedByPartitionColumn, IBlock> AttachedToRows
        {
            get { return _attachedToRows; }
        }

        /// <summary>
        /// Encapsulated drawer
        /// </summary>
        public IPartitionDrawer DecoratedDrawer
        {
            get { return decoratedDrawer; }
            set
            {
                if (decoratedDrawer != value)
                {
                    if (decoratedDrawer != null)
                    {
                        decoratedDrawer.ElementDrawn -= onElementDrawed;
                        decoratedDrawer.ElementDrawing -= onElementDrawing;
                    }

                    decoratedDrawer = value;

                    if (decoratedDrawer != null)
                    {
                        decoratedDrawer.ElementDrawn += onElementDrawed;
                        decoratedDrawer.ElementDrawing += onElementDrawing;
                    }
                }

                decoratedDrawer = value;
            }
        }

        #region IPartitionDrawer Members

        /// <summary>
        /// Draw the <paramref name="element"/> in the <paramref name="graphic"/>
        /// with <paramref name="clipBounds"/>.Location as a top left coordinate
        /// </summary>
        /// <param name="graphic">This is the graphics used to paint</param>
        /// <param name="clipBounds">This should be the maximum bounds of the element</param>
        /// <param name="element">This is the partition to paint</param>   
        public void Paint(Graphics graphic, RectangleF clipBounds, PartitionElement element)
        {
            DecoratedDrawer.Paint(graphic, clipBounds, element);

            float h = DecoratedDrawer.GetHeaderHeight(graphic) + element.Level.Height;

            var bounds = new RectangleF(clipBounds.Location, new SizeF(element.Size.Width, clipBounds.Height));

            bounds.Y += h;
            bounds.Height -= h;

            var blockCoord = new BlockIndexedByPartitionColumn(element.Column.Ordinal);

            for (int rowKey = element.Level.FirstRow; rowKey <= element.Level.LastRow; rowKey++)
            {
                blockCoord.RowIndex = rowKey;

                if (!AttachedToRows.ContainsKey(blockCoord)) continue;


                IBlock block = AttachedToRows[blockCoord];
                

                block.Draw(graphic, null, bounds);

                bounds.Y += block.GetSize(graphic, bounds.Size).Height;
            }
        }

        /// <summary>
        /// Useful to define if partitions must be horizontally centered in their bounds when painted
        /// </summary>
        public bool MustCenterHorizontally
        {
            get { return DecoratedDrawer.MustCenterHorizontally; }
            set { DecoratedDrawer.MustCenterHorizontally = value; }
        }

        /// <summary>
        /// Useful to define if partitions must be vertically centered in their bounds when painted
        /// </summary>
        public bool MustCenterVertically
        {
            get { return DecoratedDrawer.MustCenterVertically; }
            set { DecoratedDrawer.MustCenterVertically = value; }
        }

        /// <summary>
        /// Useful to indicates the order in which partitions have to be painted
        /// </summary>
        public PartitionOrder PrintOrder
        {
            get { return DecoratedDrawer.PrintOrder; }
            set { DecoratedDrawer.PrintOrder = value; }
        }

        /// <summary>
        /// get the number of rows which will be painted
        /// </summary>
        public int RowCount
        {
            get { return DecoratedDrawer.RowCount; }
        }

        /// <summary>
        /// get the number of columns which will be painted
        /// </summary>
        public int ColumnCount
        {
            get { return DecoratedDrawer.ColumnCount; }
        }

        /// <summary>
        /// Calculate the columns header height of the grid of data</summary>
        /// <param name="graphic">graphics used to render the grid of data</param>
        /// <returns>The height of the columns header of the grid of data</returns>
        public float GetHeaderHeight(Graphics graphic)
        {
            return DecoratedDrawer.GetHeaderHeight(graphic);
        }

        /// <summary>
        /// Compute the width of the [row header] column
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        /// <returns>float value, width applied the row headers</returns>
        public float RowHeaderWidth(Graphics graphic)
        {
            return DecoratedDrawer.RowHeaderWidth(graphic);
        }

        /// <summary>
        /// Get the height of a row
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        /// <param name="rowKey">index of the interesting row</param>
        /// <returns>float value, height of the interesting row</returns>
        public float RowHeight(Graphics graphic, int rowKey)
        {
            return DecoratedDrawer.RowHeight(graphic, rowKey);
        }

        /// <summary>
        /// Get the width of a column
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        /// <param name="columnKey">index of the interesting column</param>
        /// <returns>float value, width of the interesting column</returns>
        public float ColumnWidth(Graphics graphic, int columnKey)
        {
            return DecoratedDrawer.ColumnWidth(graphic, columnKey);
        }


        /// <summary>
        /// Partitions the grid of data into printable areas.
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        /// <param name="clipHeights">defines the maximum bounds for height of each partitionLevel</param>
        /// <param name="clipWidths">defines the maximum bounds for width of each partitionColumn</param>
        /// <returns>A partition of the grid to paint</returns>
        /// <seealso cref="NGridDrawer.Partitions.PartitionGrid"/>
        public PartitionGrid BuildPartition(Graphics graphic, float[] clipHeights, float[] clipWidths)
        {
            PartitionGrid partition = DecoratedDrawer.BuildPartition(graphic, clipHeights, clipWidths);

            //InitializeDrawerForGrid(graphic, clipHeights, clipWidths);

            AttachedToRows.Clear();

            var newClipHeights = new List<float>(clipHeights.Length);

            int i = 0;
            int heightsCounter = 0;


            var clipSize =
                new SizeF(
                    clipWidths[0],
                    clipHeights[heightsCounter]);

            float tmpCalculatedRowHeight = initializeRowAttach(graphic, partition, clipSize, i);

            do
            {
                float clipH = GetHeaderHeight(graphic);
                float tmpH = clipH;
                do
                {
                    clipH += RowHeight(graphic, i);

                    tmpH += tmpCalculatedRowHeight;

                    if (++i < RowCount)
                        tmpCalculatedRowHeight = initializeRowAttach(graphic, partition, clipSize, i);
                } while (i < RowCount
                         && tmpH + tmpCalculatedRowHeight <= clipSize.Height);

                newClipHeights.Insert(0, clipH);


                if (heightsCounter + 1 < clipHeights.Length)
                {
                    heightsCounter++;
                    clipSize =
                        new SizeF(
                            clipWidths[0],
                            clipHeights[heightsCounter]);
                }
            } while (i < DecoratedDrawer.RowCount);

            newClipHeights.Reverse();

            return partition.BuildPartition(newClipHeights.ToArray(), clipWidths);
        }

        ///<summary>
        /// Event raised when an element is gonna be drawn
        ///</summary>
        public event EventHandler<ElementDrawnEventArgs> ElementDrawing;

        ///<summary>
        /// Event raised when an element has been drawn
        ///</summary>
        public event EventHandler<ElementDrawnEventArgs> ElementDrawn;

        #endregion

        /// <summary>
        /// Occurs when rows are under process
        /// </summary>
        /// <remarks>
        /// Raise this event if you want to attach an <see cref="IBlock"/> instance to a row
        /// </remarks>
        public event EventHandler<RowRequestedBlockEventArgs> RowRequestedBlock;


        private void onRowRequestedBlock(RowRequestedBlockEventArgs e)
        {
            if (RowRequestedBlock != null) RowRequestedBlock(this, e);
        }

        private void onElementDrawed(object sender, ElementDrawnEventArgs e)
        {
            if (ElementDrawn != null) ElementDrawn(sender, e);
        }

        private void onElementDrawing(object sender, ElementDrawnEventArgs e)
        {
            if (ElementDrawing != null)
                ElementDrawing(sender, e);
        }


        private float initializeRowAttach(
            Graphics graphic, PartitionGrid partition,
            SizeF clipSize, int rowKey)
        {
            float attachedNoteHeight = float.MinValue;

            for (int pcolId = 0; pcolId < partition.PartitionColumnCount; pcolId++)
            {
                PartitionColumn pcol = partition.PartitionColumn(pcolId);
                // warning with rowkey, it's the ordinal number of the row which will be printed.
                var e = new RowRequestedBlockEventArgs(pcol, rowKey);


                onRowRequestedBlock(e);

                if (e.AttachedToRow != null)
                {
                    var blockCoord = new BlockIndexedByPartitionColumn(pcol.Ordinal, rowKey);

                    AttachedToRows.Add(blockCoord, e.AttachedToRow);

                    attachedNoteHeight = Math.Max(e.AttachedToRow.GetSize(graphic, clipSize).Height, attachedNoteHeight);
                }
            }

            if (attachedNoteHeight == float.MinValue)
                return RowHeight(graphic, rowKey);

            return attachedNoteHeight + RowHeight(graphic, rowKey);
        }

        #region Nested type: BlockIndexedByPartitionColumn

        /// <summary>
        /// Provide an easy way to index a part of a grid row in order to use it as a key in a Dictionary
        /// </summary>
        /// <remarks>this code is not intended to be used by developers</remarks>
        protected class BlockIndexedByPartitionColumn : IEquatable<BlockIndexedByPartitionColumn>
        {
            /// <summary>
            /// 	<para>Initializes an instance of the <see cref="GridAnnotatedDrawer"/> class.</para>
            /// </summary>
            /// <param name="partitionColumnId">Id of the concerned <see cref="PartitionColumn"/> instance
            /// </param>
            public BlockIndexedByPartitionColumn(int partitionColumnId)
            {
                PartitionColumnId = partitionColumnId;
            }

            /// <summary>
            /// 	<para>Initializes an instance of the <see cref="GridAnnotatedDrawer"/> class.</para>
            /// </summary>
            /// <param name="partitionColumnId">Id of the concerned <see cref="PartitionColumn"/> instance
            /// </param>
            /// <param name="rowIndex">Id of the concerned concerned grid row</param>
            public BlockIndexedByPartitionColumn(int partitionColumnId, int rowIndex) : this(partitionColumnId)
            {
                RowIndex = rowIndex;
            }


            ///<summary>
            ///Get/Set row index property
            ///</summary>
            public int RowIndex { get; set; }


            ///<summary>
            ///Get/Set partition column index property
            ///</summary>
            public int PartitionColumnId { get; set; }

            #region IEquatable<BlockIndexedByPartitionColumn> Members

            ///<summary>
            ///Indicates whether the current object is equal to another object of the same type.
            ///</summary>
            ///
            ///<returns>
            ///true if the current object is equal to the <paramref name="other" /> parameter; otherwise, false.
            ///</returns>
            ///
            ///<param name="other">An object to compare with this object.</param>
            public bool Equals(BlockIndexedByPartitionColumn other)
            {
                if (ReferenceEquals(null, other)) return false;
                if (ReferenceEquals(this, other)) return true;
                return other.RowIndex == RowIndex && other.PartitionColumnId == PartitionColumnId;
            }

            #endregion

            ///<summary>
            ///Determines whether the specified <see cref="T:System.Object" /> is equal to the current <see cref="T:System.Object" />.
            ///</summary>
            ///
            ///<returns>
            ///true if the specified <see cref="T:System.Object" /> is equal to the current <see cref="T:System.Object" />; otherwise, false.
            ///</returns>
            ///
            ///<param name="obj">The <see cref="T:System.Object" /> to compare with the current <see cref="T:System.Object" />. </param>
            ///<exception cref="T:System.NullReferenceException">The <paramref name="obj" /> parameter is null.</exception><filterpriority>2</filterpriority>
            public override bool Equals(object obj)
            {
                if (ReferenceEquals(null, obj)) return false;
                if (ReferenceEquals(this, obj)) return true;
                if (obj.GetType() != typeof (BlockIndexedByPartitionColumn)) return false;
                return Equals((BlockIndexedByPartitionColumn) obj);
            }

            ///<summary>
            ///Serves as a hash function for a particular type. 
            ///</summary>
            ///
            ///<returns>
            ///A hash code for the current <see cref="T:System.Object" />.
            ///</returns>
            ///<filterpriority>2</filterpriority>
            public override int GetHashCode()
            {
                unchecked
                {
                    return (RowIndex*397) ^ PartitionColumnId;
                }
            }
        }

        #endregion

    }
}